# -*- coding: utf-8 -*-
# launcher.py
# 本地开发环境一键启动脚本
# 功能：自动启动 Redis、Nacos 等服务，检查端口占用，新窗口运行，支持 Ctrl+C 统一停止
# 作者：Qwen
# 日期：2025-07-31

import subprocess  # 用于创建和管理子进程（启动服务）
import logging     # 日志记录
import time        # 时间相关操作，如等待、休眠
import os          # 操作系统接口，处理文件路径、环境变量等
import socket      # 网络编程，用于检查端口是否开放
import sys         # 系统相关参数和函数
import io          # 输入输出流控制
import yaml        # 用于解析 YAML 格式的配置文件

# ================ Windows 中文乱码修复 ================
# Windows CMD 默认使用 GBK 编码，而脚本是 UTF-8，会导致中文乱码
if sys.platform == "win32":  # 仅在 Windows 系统下执行
    try:
        from ctypes import windll
        # 设置控制台输入和输出代码页为 UTF-8 (65001)
        kernel32 = windll.kernel32
        kernel32.SetConsoleOutputCP(65001)  # 输出编码
        kernel32.SetConsoleCP(65001)        # 输入编码
    except:
        pass  # 如果设置失败，忽略（可能缺少权限或库）

# 强制 Python 的标准输出和错误输出使用 UTF-8 编码
# 避免日志中的中文在文件或控制台中显示为乱码
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

# ================ 资源路径处理（PyInstaller 打包兼容）===============
def resource_path(relative_path):
    """
    获取资源的绝对路径，兼容开发环境和 PyInstaller 打包环境。
    PyInstaller 在打包时会将资源文件解压到一个临时的 _MEIPASS 目录中。
    :param relative_path: 相对于脚本文件的资源路径（如 "config.yaml", "logs"）
    :return: 资源的绝对路径
    """
    try:
        # PyInstaller 创建一个临时文件夹，并将路径存储在 _MEIPASS 中
        base_path = sys._MEIPASS
    except Exception:
        # 在开发环境中，base_path 就是脚本所在目录
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

# ================ 日志配置 ================
# 获取脚本所在目录的绝对路径（兼容打包环境）
script_dir = resource_path("")  # 使用 resource_path 获取基础路径
# 在脚本同级目录下创建 logs 文件夹（路径兼容打包）
logs_dir = os.path.join(script_dir, "logs")
os.makedirs(logs_dir, exist_ok=True)  # 如果目录不存在则创建

# 日志文件路径（使用兼容路径）
log_file = os.path.join(logs_dir, "launcher.log")

# 配置日志系统
logging.basicConfig(
    level=logging.INFO,  # 日志级别：INFO 及以上会被记录
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',  # 日志格式
    handlers=[
        # 处理器1：写入日志文件，使用 UTF-8 编码
        logging.FileHandler(log_file, encoding='utf-8'),
        # 处理器2：输出到控制台（stdout）
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)  # 获取当前模块的日志记录器

# ================ 服务配置数据类 ================
class ServiceConfig:
    """存储单个服务的配置信息"""
    def __init__(self, name: str, executable: str, args: list, working_dir: str,
                 check_port: int, env_vars: dict = None, startup_timeout: int = 30,
                 auto_start: bool = True, description: str = ""):
        self.name = name              # 服务名称
        self.executable = executable  # 可执行文件名（如 cmd, java.exe）
        self.args = args              # 命令行参数列表
        self.working_dir = working_dir # 服务的工作目录（程序所在路径）
        self.check_port = check_port  # 监听端口，用于健康检查
        self.env_vars = env_vars or {} # 环境变量字典
        self.startup_timeout = startup_timeout # 启动超时时间（秒）
        self.auto_start = auto_start  # 是否自动启动
        self.description = description # 描述

# ================ 配置文件加载器 ================
class ConfigLoader:
    """从 YAML 文件加载服务配置"""
    @staticmethod
    def load(config_path: str) -> list:
        """
        加载并解析 YAML 配置文件
        :param config_path: 配置文件的绝对路径
        :return: ServiceConfig 对象列表
        """
        with open(config_path, 'r', encoding='utf-8') as f:
            data = yaml.safe_load(f)  # 安全地解析 YAML 内容
        services = []
        for item in data.get('services', []):  # 遍历 services 列表
            # 创建服务配置对象
            svc = ServiceConfig(
                name=item['name'],
                executable=item['executable'],
                args=item['args'],
                working_dir=item['working_dir'],
                check_port=item['check_port'],
                env_vars=item.get('env_vars'),  # get() 避免 KeyError
                startup_timeout=item.get('startup_timeout', 30),
                auto_start=item.get('auto_start', True),
                description=item.get('description', '')
            )
            services.append(svc)
        return services

# ================ 服务管理器 ================
class ServiceManager:
    """管理所有服务的启动、停止和状态"""
    def __init__(self):
        self.services = {}    # 存储所有服务配置 {name: ServiceConfig}
        self.processes = {}   # 存储正在运行的服务进程 {name: subprocess.Popen}

    def add(self, svc: ServiceConfig):
        """添加一个服务配置"""
        self.services[svc.name] = svc

    def _is_port_open(self, host: str, port: int) -> bool:
        """
        检查指定主机和端口是否处于监听状态（即端口是否被占用）
        :param host: 主机地址，通常为 "127.0.0.1"
        :param port: 端口号
        :return: True 表示端口已开放（被占用），False 表示空闲
        """
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(1)  # 设置 1 秒超时
            # connect_ex 返回 0 表示连接成功（端口开放）
            return s.connect_ex((host, port)) == 0

    def _wait_port(self, host: str, port: int, timeout: int) -> bool:
        """
        等待指定端口变为开放状态，最多等待 timeout 秒
        用于确认服务是否成功启动并开始监听
        :param host: 主机
        :param port: 端口
        :param timeout: 超时时间（秒）
        :return: True 表示在超时前端口开放，False 表示超时仍未开放
        """
        for _ in range(timeout):
            if self._is_port_open(host, port):
                return True
            time.sleep(1)  # 每秒检查一次
        return False

    def start_service(self, name: str) -> bool:
        """
        启动指定名称的服务
        :param name: 服务名称
        :return: 启动成功返回 True，失败返回 False
        """
        if name not in self.services:
            logger.error(f"服务不存在: {name}")
            return False

        svc = self.services[name]
        if not svc.auto_start:
            logger.info(f"跳过: {name} (非自动启动)")
            return True  # 跳过不算失败

        # ✅ 启动前检查：目标端口是否已被其他程序占用
        if svc.check_port and self._is_port_open("127.0.0.1", svc.check_port):
            logger.warning(f"⚠️  端口 {svc.check_port} 已被占用 → {name}")
            logger.warning(f"    请检查是否有其他 {name.upper()} 实例正在运行")
            return True  # 端口被占用，跳过启动，但不标记为失败

        # 构造启动时使用的环境变量
        # 从当前环境复制，然后更新服务特定的变量
        env = os.environ.copy()
        for k, v in svc.env_vars.items():
            # 路径分隔符标准化（Windows 使用反斜杠）
            env[k] = v.replace('/', '\\')

        # 构建完整的命令行
        # 例如: ['cmd', '/k', 'title', 'REDIS', '&&', 'redis-server.exe']
        final_cmd = [svc.executable] + svc.args
        # 工作目录路径标准化
        work_dir = svc.working_dir.replace('/', '\\')

        try:
            # 创建子进程，启动服务
            proc = subprocess.Popen(
                final_cmd,           # 要执行的命令和参数
                cwd=work_dir,        # 工作目录
                env=env,             # 环境变量
                # 关键参数：创建一个全新的控制台窗口
                creationflags=subprocess.CREATE_NEW_CONSOLE
                # 注意：这里没有捕获 stdout/stderr，因为希望在新窗口直接显示
            )
            # 保存进程对象，便于后续停止
            self.processes[name] = proc
            logger.info(f"✅ 启动 {name} | PID={proc.pid} | 命令: {' '.join(final_cmd)}")

            # 如果配置了检查端口，则等待服务启动完成
            if svc.check_port:
                if self._wait_port("127.0.0.1", svc.check_port, svc.startup_timeout):
                    logger.info(f"🟢 {name} 成功运行在端口 {svc.check_port}")
                else:
                    logger.error(f"🔴 {name} 启动超时 (>{svc.startup_timeout}s)")
                    return False  # 启动失败
            return True
        except Exception as e:
            # 捕获所有异常，如文件未找到、权限不足等
            logger.error(f"❌ 启动 {name} 失败: {e}")
            return False

    def start_all(self) -> dict:
        """
        启动所有配置的服务
        :return: 启动结果字典，如 {"redis": "成功", "nacos": "失败"}
        """
        results = {}
        for name in self.services:
            success = self.start_service(name)
            results[name] = "成功" if success else "失败"
        return results

    def stop_all(self):
        """停止所有正在运行的服务"""
        for name, proc in self.processes.items():
            try:
                # 先尝试优雅终止
                proc.terminate()
                # 最多等待 5 秒，让进程自行清理
                proc.wait(timeout=5)
                logger.info(f"⏹️ 停止 {name}")
            except Exception as e:
                logger.error(f"停止 {name} 失败: {e}")

# ================ 主函数 ================
def main():
    # 使用 resource_path 获取基础路径，兼容打包环境
    script_dir = resource_path("")
    # 构建配置文件的绝对路径
    config_path = os.path.join(script_dir, "config.yaml")

    # 检查配置文件是否存在
    if not os.path.exists(config_path):
        logger.error(f"❌ 配置文件不存在: {config_path}")
        print(f"请确认文件是否存在，并放在脚本同目录下。\n路径: {config_path}")
        return

    # 创建服务管理器
    manager = ServiceManager()

    # 尝试加载配置
    try:
        configs = ConfigLoader.load(config_path)
        for cfg in configs:
            manager.add(cfg)
    except Exception as e:
        logger.error(f"❌ 加载配置失败: {e}")
        return

    # 开始启动服务
    print("🚀 正在启动本地开发环境...")
    results = manager.start_all()

    # 打印启动结果汇总
    print("\n" + "="*40)
    print("        启动结果")
    print("="*40)
    for svc, status in results.items():
        icon = "✅" if status == "成功" else "❌"
        print(f"  {icon} {svc:<10} : {status}")
    print("="*40)

    # 提示信息
    print("\n📌 所有服务已发送启动指令。")
    print("📌 Redis 默认端口: 6379")
    print("📌 Nacos 访问地址: http://localhost:8848/nacos  (账号: nacos/nacos)")
    print("📌 按 Ctrl+C 停止所有服务。")

    # 主程序保持运行，等待用户中断（Ctrl+C）
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n\n🛑 正在停止所有服务...")
        manager.stop_all()
        print("👋 所有服务已停止，再见！")

# 程序入口点
# 当直接运行此脚本时，执行 main()
if __name__ == "__main__":
    main()